package com.hhf.riceshopping.ware.service.impl;

import com.alibaba.fastjson.TypeReference;
import com.hhf.common.exception.NoStockException;
import com.hhf.common.to.mq.OrderTo;
import com.hhf.common.to.mq.StockDetailTo;
import com.hhf.common.to.mq.StockLockedTo;
import com.hhf.common.utils.R;
import com.hhf.riceshopping.ware.entity.WareOrderTaskDetailEntity;
import com.hhf.riceshopping.ware.entity.WareOrderTaskEntity;
import com.hhf.riceshopping.ware.feign.OrderFeignService;
import com.hhf.riceshopping.ware.feign.ProductFeignService;
import com.hhf.riceshopping.ware.service.WareOrderTaskDetailService;
import com.hhf.riceshopping.ware.service.WareOrderTaskService;
import com.hhf.riceshopping.ware.vo.*;
import com.rabbitmq.client.Channel;
import lombok.Data;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hhf.common.utils.PageUtils;
import com.hhf.common.utils.Query;

import com.hhf.riceshopping.ware.dao.WareSkuDao;
import com.hhf.riceshopping.ware.entity.WareSkuEntity;
import com.hhf.riceshopping.ware.service.WareSkuService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Autowired
    WareSkuDao wareSkuDao;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    WareOrderTaskService orderTaskService;

    @Autowired
    WareOrderTaskDetailService orderTaskDetailService;

    @Autowired
    OrderFeignService orderFeignService;

    /**
     * 库存自动解锁
     * 下订单成功，库存也成功锁定库存，但是接下来的业务调用失败，导致订单回滚。之前锁定的库存就要进行自动解锁并回补库存
     * 但是如果订单失败，本身就是锁库存失败导致的，也就是有一个商品没锁住
     */


    //解锁库存
    private void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId){
        wareSkuDao.unLockStock(skuId,wareId,num);
        //只要库存解锁，那么库存工作单的状态也应该更新为已解锁的状态
        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2);//已解锁
        orderTaskDetailService.updateById(entity);
    }


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        /**
         * wareId: 123,//仓库id
         *    skuId: 123//商品id
         */
        QueryWrapper<WareSkuEntity> queryWrapper = new QueryWrapper<>();
        String skuId = (String) params.get("skuId");
        if (!StringUtils.isEmpty(skuId)){
            queryWrapper.eq("sku_id",skuId);
        }

        String wareId = (String) params.get("wareId");
        if (!StringUtils.isEmpty(wareId)){
            queryWrapper.eq("ware_id",wareId);
        }

        IPage<WareSkuEntity> page = this.page(
                new Query<WareSkuEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

    @Override
    public void addStock(Long skuId, Long wareId, Integer skuNum) {
        //判断如果还没有这个库存记录，那就是新增操作
        List<WareSkuEntity> entities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
        if (entities == null || entities.size()==0){  //那就是添加操作
            WareSkuEntity skuEntity = new WareSkuEntity();
            skuEntity.setSkuId(skuId);
            skuEntity.setWareId(wareId);
            skuEntity.setStock(skuNum);
            skuEntity.setStockLocked(0);
            //这里如果事务失败了，事务也不需要回滚
            try {
                //远程查询sku的名字
                R info = productFeignService.info(skuId);
                Map<String, Object> data = (Map<String, Object>) info.get("skuInfo");
                if (info.getCode() == 0){  //远程调用成功才设置值
                    skuEntity.setSkuName((String) data.get("skuName"));
                }
            }catch (Exception e){

            }


            wareSkuDao.insert(skuEntity);
        }else {
            //如果有这个库存记录那就是更新操作
            wareSkuDao.addStock(skuId,wareId,skuNum);
        }

    }

    @Override
    public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
        List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
            SkuHasStockVo vo = new SkuHasStockVo();

            //查询sku的总库存量 SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id=40
            Long count = baseMapper.getSkuStock(skuId);
            vo.setSkuId(skuId);
            vo.setHasStock(count==null?false:count>0);  //有库存
            return vo;
        }).collect(Collectors.toList());
        return collect;
    }

    /**
     * 为某个订单商品锁定库存
     *
     * 锁定库存的时候，每锁一个什么商品的库存，都给数据库表里边先来记录一下
     * 比如库存的数据库有两张表：
     *      wms_ware_order_task：库存工作单，用于某个订单想要锁库存
     *      wms_ware_order_task_detail：库存工作单详情，用于某个订单想要锁库存锁成什么样。比如每一个商品锁几件包括在哪个仓库锁的
     *  所以相当于只要锁库存，先给数据库里边保存记录，要锁库存
     *  只要锁库存成功了一切OK
     *   如果锁失败了数据库就相当于没有锁库存记录，因为本身锁库存失败会全部回滚
     *   但是如果是这种失败，库存锁成功了，然后接下来继续执行其他订单操作的时候，出现异常了，那么此时库存需要进行解锁
     *   那么相当于库存数据库里边就有当时锁库存的记录，可以使用定时任务，每隔多少分钟去扫描一下这个锁库存的记录的数据库
     *   看看锁库存的wms_ware_order_task_detail记录数据表里边哪些订单是已经被取消了或者是已经被回滚了，
     *   那么锁库存消息还在wms_ware_order_task_detail库存工作单详情表里边存着，那就把这些消息拿出来，把库存重新进行解锁
     *   定时任务去定时扫描也是一个很麻烦的事情，所以要加入一个消息中间件当中的延时队列
     *   这个延时队列是用来做定时功能，比如库存锁成功了，下订单失败了，锁库存要自己解锁，那么就把锁成功的消息发给消息队列，
     *   但是让消息队列先别让解锁库存服务收到，在消息队列里边暂存一会（比如存上30分钟），因为现在订单可能是成功了也有可能是失败了
     *     无论是成功还是失败，30分钟以后订单不支付，也都把它关了，所以30分钟以后订单就见分晓了（它是成的还是失败的）
     *     如果30分钟以后再把这个消息发给解锁库存服务，解锁库存服务一看，这个订单早都没有了或者是订单都没有支付被人取消了
     *     就可以把当时锁的这个库存，自动进行解锁一下，相当于就整了一个定时的任务。
     *     库存的解锁消息，订单几十分钟失效以后才开始进行解锁
     *     想要完成这个功能就要用到延时队列
     *
     *     RabbitMQ延时队列来实现定时任务的效果
     *     比如延时队列的一个场景
     *      系统里边首先下订单，正常是订单下成功以后，如果30分钟左右还没有支付，系统就会自动的进行关单操作
     *      关单之后，这个订单以前锁定的库存肯定要解锁。
     *      那订单30分钟以后未支付就关闭订单，这个操作可以用定时任务来做，假设用定时任务可以每隔30分钟扫描一次数据库
     *      看数据库哪些订单还没有支付，如果还没有支付而且到期了就把它进行一个关单操作
     *
     *      关单之后就要进行解锁对应的库存，所以下订单之后需要30分钟还没支付就要进行关单，然后接下来
     *      锁库存：也就是订单下单成功，接下来就调用锁库存方法，然后再调用其他的方法继续执行
     *      但万一锁库存成功了，订单也本身成功了，但别的方法调用失败了，那么订单自己会回滚操作，那就不存在关单的问题
     *      但库存服务如果使用seata来控制分布式事务，它就也得跟着回滚，但为了高并发期间，就不用seata来控制
     *      锁定的这些库存，也能进行解锁，就可以设置一个定时任务，比如40分钟以后来进行一个检查。
     *      为啥要设置40分钟呢？
     *      因为订单关单的时间是30分钟，想要检查的话，那么这个订单可能是成功的，所以在确保所有的订单肯定都没问题以后，
     *      再来检查这些订单的库存状态，如果扫描这些订单的库存状态，若发现刚锁的这个库存对应的这个订单根本就没有
     *      那订单不存在那就是在订单下单的时候出现异常回滚了，或者这个订单已经被人给取消了，那说明这个订单它没有进行一个支付
     *      此时就需要把锁定的库存进行一个解锁，所以锁库存到解锁库存这个阶段也可以做一个定时任务
     *      但是都来用定时任务就会带来很大的问题
     *      首先定时任务消耗系统内存，增加了数据库的压力（毕竟定时任务每隔一段时间都要去轮询扫描一下数据库，
     *      那数据库每隔一段时间就要进行全盘扫描，这样整个数据库的压力就会非常大），
     *      。而且定时任务最致命的问题就是存在较大的时间误差，比如每隔30分钟扫一次数据库，那假设第一次下单，订单是在第一分钟下的订单
     *      但是定时任务刚运行完，也就是在订单下单的前一分钟运行完，而这个下单是时间随机的，也不知道啥时候下单
     *      那么上一次定时任务刚下完，那一分钟过后订单就下单了，那么再隔上30分钟，定时任务去扫数据库，那么巧了，这个下订单还没有经过30分钟的这个
     *      煎熬期，只有在30分钟之后没支付才能关，所以还差一分钟，而刚好定时任务扫到了这个订单，但发现这个订单不符合条件，那就不会给它进行一个关单操作
     *      结果定时任务刚一结束，一分钟过后订单就自己超时了，但是发现上一次定时任务发现这个订单不符合要求就把他放过了，结果刚一放过，订单就超时了
     *      订单超时以后。这个订单就需要在下一次的定时任务才能被扫到了，那么这样等待的时间就很长了，所以这就是定时任务出现的时效性问题
     *      但这样如果业务可以的话，这种情况其实是可以被允许的，因为总之再怎么等，这个超时的订单最终都会被定时任务扫到而进行关单解锁库存
     *      主要就看业务情况是否允许出现定时任务的时间误差问题
     *
     *      如果定时任务的时间误差问题是在业务当中不允许的，那就可以采用MQ的延时队列
     *         延时队列：
     *             是基于消息的TTL（存活时间）和死信exchange(路由)结合的
     *             如果有了延时对了，那么整个工作流程应该是这样的
     *      比如下订单，订单一下成功以后，就给消息队列里边发一个消息说哪个订单下成功了，然后这一块若是定时任务那么就是30分钟以后扫描关单
     *      ，那么现在就不需要定时任务了，发的这个消息发到消息队列里边之后，这个消息队列的消息最大的特点就是，这些下订单成功的消息在30分钟以后才能被人收到
     *      如果有一个服务专门来监听这个队列，那么接下来消息里边存来的订单，下成功的消息，30分钟以后才会来到监听服务这里
     *      那这样监听者30分钟之后拿到这个订单的消息，再一查结果30分钟都到了，消息都抵达监听者这一块了还没有支付，那么就进行关单操作了
     *      所以整个过程无需定时任务，让MQ把消息在存一段时间
     *      包括锁库存也是一样的，只要库存锁成功了就给MQ发一个消息，然后先让MQ把这个消息保存一段时间，然后到了一段时间以后MQ再把消息发出去。
     *      然后解锁库存服务监听到了消息，就按照这个消息检查订单，订单没支付或者早都没有了，那就进行一个解锁库存的操作
     *
     *      所以如果使用延时队列就能够解决定时任务大面积的时效性问题，而延时队列时效性就算差那么一秒或者几秒甚至一分钟都不可能差上很久
     *      所以基于整个系统以及业务的考虑，我们这边就使用的是MQ的延时队列来做下订单和关单以及锁库存和解锁库存操作，最终保证事务的最终一致性
     *
     *      引入MQ的第一个目的就是解决事务的最终一致性问题，因为订单最终还是要关单的，所以使用MQ来暂缓一段时间消息，不占用系统的任何资源
     *      只是多加入一个MQ服务器，等时间到了以后就能够保证数据的最终一致，这就是MQ延时队列的使用
     *
     *      MQ延时队列的使用：
     *          延时队列是消息的TTL（存活时间）和死信exchange(路由)结合的
     *          对队列和消息都可以设置存活时间，无论队列和消息设置存活时间，其实都是一个意思，那就是消息只要在指定的时间里边没有被人消费
     *          那这个消息就相当于没用了，就可以把这个过期的消息称为死信，而这个消息就可以作为单独处理
     *          所以如果是给队列的消息设置了过期时间，如果这个消息一直没有被监听，如果一监听肯定就拿到了，如果这个队列里边消息没有任何去消费
     *          ，只要30分钟一过那这消息就成为了死信，没人要的消息，那就可以把它扔掉了，服务器就会默认把它进行丢弃
     *          所以设置消息的TTL目的就是只要在一定时间，这个时间内没有任何消费者去消费这个消息，就会被服务器认为这是一个死信
     *          然后就把这个消息放到死信队列
     *
     *
     *     库存解锁的场景，也就是什么情况下要进行解锁？
     *          1、 下订单成功，订单过期没有支付被系统自动取消了或者被用户手动取消订单就要进行解锁库存
     *          2、 下订单成功，库存也成功锁定库存，但是接下来的业务调用失败，导致订单回滚。
     *              之前锁定的库存就要进行自动解锁并回补库存
     *
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {

        /**
         * 想要准备锁库存之前，先来保存库存工作单的详情信息
         * 引入这个库存工作单的详情信息是为了追溯哪个订单的商品在哪个库存锁了多少件等，以防出问题，人为的回滚也就方便
         *
         *   wms_ware_order_task 库存工作单：表示要为某一个订单准备要锁库存了
         *  wms_ware_order_task_detail 库存工作单详情  表示：这个订单哪个商品在哪个仓库锁了几件
         */
        //先来保存wms_ware_order_task库存工作单
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());
        orderTaskService.save(taskEntity);

        //1 按照下单的收货地址，找到一个就近仓库。然后锁定库存
        //2 找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            //先来找一下这个商品在哪个仓库都有库存
            Long skuId = item.getSkuId();
            SkuWareHasStock stock = new SkuWareHasStock();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);//只需要查询出所有仓库的id
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

        //锁定库存
        for (SkuWareHasStock hasStock : collect) {
            Boolean skuStocked = false;  //默认没被锁定
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            if (wareIds == null || wareIds.size() == 0){
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //说明有库存，就来扣减每一个仓库
            //如果每一个商品都锁定成功，将当期商品锁定了几件的工作单详情记录发送给MQ
            //如果是锁定失败，前面保存的工作单信息就回滚了，那么MQ发送消息给对方服务也没有问题，即使要解锁记录
            //由于已经回滚了就去数据库查询不了Id，就不知道当时锁的是多少个库存，也就不用自动解锁了  但这样是不合理的
            for (Long wareId : wareIds) {
                /**
                 * skuId :锁哪个商品
                 * wareId: 锁哪个仓库的
                 * hasStock.getNum():锁几件
                 */
                //成功就返回1,否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
                if (count == 1){
                    //锁定成功了
                    skuStocked = true;
                    // wms_ware_order_task_detail 库存工作单详情  表示：这个订单哪个商品在哪个仓库锁了几件,1是锁定库存成功的状态
                        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
                    orderTaskDetailService.save(entity);
                    //如果锁定成功了就没必要锁其他仓库了，并且给MQ发送一个消息，告诉MQ库存锁定成功
                    //发送给MQ的消息内容是：哪一个库存工作单到底锁了多少个商品信息
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(entity,stockDetailTo);
                    //要发全量信息，防止系统异常回滚以后找不到数据
                    lockedTo.setDetail(stockDetailTo);
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    break;
                }else {
                    //当前仓库库存锁定失败了。那就继续重试下一个仓库
                }
            }
                //如果当前商品所有仓库都没有锁住
            if (skuStocked ==false){
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
        }
        //肯定全部都是锁定成功的


        return true;
    }

    @Override
    public void unlockStock(StockLockedTo to) {

            StockDetailTo detail = to.getDetail();
            Long skuId = detail.getSkuId();
            Long detailId = detail.getId();
            //查询数据库关于这个订单的锁定库存消息
            //如果数据库有锁定库存消息,证明库存是锁定成功的，但是到底要不要解锁，还是要看订单情况
            //解锁：订单情况
            //1.没有这个订单，必须解锁
            //2.如果有这个订单，不是直接解锁库存，要看订单状态，订单状态如果是已取消或者已超时那就进行解锁库存
            WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
            if (byId != null) {
                //进行解锁
                Long id = to.getId();//库存工作单的id
                WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
                String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态
                R r = orderFeignService.getOrderStatus(orderSn);
                if (r.getCode() == 0) {
                    //订单数据返回成功
                    OrderVo data = r.getData(new TypeReference<OrderVo>() {
                    });
                    if (data == null || data.getStatus() == 4) {
                        //订单已经被取消了,才能解锁库存
                        //或者没有查到订单，订单不存在，库存服务已经锁好库存了，但是订单调用其他方法的时候炸了，导致订单回滚了，那么库存还要进行解锁
                        if (byId.getLockStatus()  == 1 ){//当期库存工作单详情，状态1是已锁定但是未解锁才可以解锁
                            unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                        }
                    }
                } else {

                    throw new RuntimeException("远程服务失败");
                }
            } else {
                //无需解锁
            }
    }

    //解锁订单，防止订单服务卡顿导致订单状态消息一直改不了，库存消息又优先到期，查订单状态很可能就是新建状态，那么库存就什么都不做就溜了
    //导致卡顿等原因的订单永远无法解锁库存
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        //根据库存工作单按照订单号，找到库存工作单的id，再找到库存工作详情里边哪些商品在哪些库存进行锁定了，
       WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.getId();
        //按照库存工作单找到所有没有解锁的库存进行解锁,
        // 并且锁了几件，并且要在库存工作详情里边保证商品状态是没有被解锁过的,是新建状态的，所以必须先查一下最新的库存解锁状态，防止重复解锁
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));
        //Long skuId,Long wareId,Integer num,Long taskDetailId
        for (WareOrderTaskDetailEntity entity : entities) {
            unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
        }

    }

    //拥有仓库的商品
    @Data
    class SkuWareHasStock {
        private Long skuId; //商品的id
        private Integer num; //锁多少件
        private List<Long> wareId;  //都在哪个仓库有库存
    }

}